package hydrap2p.library;

import hydrap2p.ServiceLocation;
import hydrap2p.logging.Level;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Map.Entry;

import org.apache.thrift.TException;

/**
 * Responsible for maintaining a list of active metadaters, and for forwarding metadating requests to each metadater
 * @author Hufman
 *
 */
public class Hashers {
	private Map<String, Hasher> hashers;
	private List<NewHashListener> listeners;
	
	private volatile static Hashers instance=null;
	
	public Hashers() {
		hashers=new TreeMap<String, Hasher>();
		listeners=new LinkedList<NewHashListener>();
	}

	public static Hashers getInstance()
	{
		if (instance==null){
			synchronized(Hashers.class)
			{
				if (instance==null)
					instance=new Hashers();
			}
		}
		return instance;
	}
	
	public static void shutdown()
	{
		instance=null;
	}
	
	public boolean subscribe(String name, ServiceLocation location)
	{
		Hasher hasher;
		try {
			hasher=new HasherThrift(location);
		} catch (TException e) {
			LibraryLogging.log(Level.MISHAP, "Library Hashers", "Failed to connect to Hasher "+name+": "+e.getLocalizedMessage());
			return false;
		}
		
		synchronized(hashers)
		{
			hashers.put(name, hasher);
		}
		newHash(name);
		return true;
	}
	
	/**
	 * Method to help in adding dummy Metadaters which don't have to be full Thrift servers
	 * @param name
	 * @param metadater
	 */
	public void subscribe(String name, Hasher hasher)
	{
		synchronized(hashers)
		{
			hashers.put(name, hasher);
		}
		newHash(name);
	}
	
	public boolean unsubscribe(String name)
	{
		boolean ret=false;
		synchronized(hashers)
		{
			ret=hashers.containsKey(name);
			if (ret)
				hashers.remove(name);
		}
		return ret;
	}
	
	public List<String> getHashers()
	{
		return new LinkedList<String>(hashers.keySet());
	}
	
	public void registerListener(NewHashListener listener)
	{
		synchronized(listeners)
		{
			listeners.add(listener);
		}
	}
	public void unregisterListener(NewHashListener listener)
	{
		synchronized(listeners)
		{
			listeners.remove(listener);
		}
	}
	private void newHash(String algorithm)
	{
		synchronized(listeners)
		{
			for (NewHashListener listener:listeners)
			{
				listener.newHash(algorithm);
			}
		}
	}
	
	/**
	 * Scans for hashes on this file
	 * If force==true, rescan metadata from providers that have returned metadata in the past
	 * @param item
	 * @param force
	 */
	public boolean scanHashes(ItemInfo item, boolean force)
	{
		boolean changed=false;
		Map<String, Hasher> myhashers;
		List<String> failedhashers = new LinkedList<String>();
		List<String> newhashers = new LinkedList<String>();				// what the new list of hashes should look like
		List<HashDataEx> newhashes = new LinkedList<HashDataEx>();	// the new list of hashes
		synchronized(hashers)
		{
			myhashers = new TreeMap<String, Hasher>(hashers);
		}
		
		if (!force)
		{
			newhashes.addAll(item.finishedHashesEx);		// load previous metadata, if we're not forcing a complete scan
			newhashers.addAll(item.hashers);	// load list of previous metadaters
		}
		
		for (Entry<String, Hasher> entry : myhashers.entrySet())
		{
			String name=entry.getKey();
			if (!newhashers.contains(name))	// need to parse this for metadata
			{
				try {
					// Load this hasher's hashes
					List<HashData> incomingHashes=entry.getValue().getHashes(item);
					for (HashData incomingHash:incomingHashes)
					{
						newhashes.add(new HashDataEx(incomingHash,true,true));
					}
					newhashers.add(name);
					changed=true;
				} catch (TException e) {
					// Remove this hasher, because it failed
					LibraryLogging.log(Level.INFO, "Library Hashers", "Failed to connect to Hasher "+name+" during scan of "+item.getPathname()+": "+e.getLocalizedMessage());
					failedhashers.add(name);
				} catch (Exception e) {
					LibraryLogging.log(Level.INFO, "Library Hashers", "Failed to connect to Hasher "+name+" during scan of "+item.getPathname()+": "+e.getLocalizedMessage());
					e.printStackTrace();
				}
			}
		}
		item.finishedHashesEx=newhashes;
		item.hashers=newhashers;
		item.downconvertHashData();
		
		synchronized(hashers)
		{
			hashers.keySet().removeAll(failedhashers);
		}
		return changed;
	}
	
	/**
	 * Just returns whether this HashData is valid
	 * @param item
	 * @param whichhash
	 * @return
	 */
	public boolean verifyHash(ItemInfo item, HashData hash)
	{
		Hasher hasher;
		synchronized(hashers)
		{
			hasher=hashers.get(hash.algorithm);
		}
		try {
			String curhash=hasher.getHash(item, hash.range);
			if (hash.hashdata.equals(curhash))
			{
				return true;
			}
		} catch (Exception e) {
			// Could not connect to this hasher
			unsubscribe(hash.algorithm);
		}
		return false;
	}
	
}
